package ee.ut.re;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import weka.core.Attribute;
import weka.core.FastVector;
import weka.core.Instance;
import weka.core.Instances;

public class NonWekaDistanceTest {

	
	static interface Function<T> {
		void exec(T t);
	}
	
	static <T> void forEach(Collection<T> collection, Function<T> func) {
		for (T t : collection) {
			func.exec(t);
		}
	}
	
	static final PriceIndex priceIndex = new PriceIndex();
	static final Attribute ID_ATTR = new Attribute("id");
	static final Attribute PRICE_ATTR = new Attribute("price");
	static final Attribute YLDPIND_ATTR = new Attribute("yldpind");
	static final Attribute SAUN_ATTR = new Attribute("saun");
	static final Attribute RODU_ATTR = new Attribute("RODU");
	static final Attribute ELKKYTE_ATTR = new Attribute("ELKKYTE");
	static final Attribute KESKKYTE_ATTR = new Attribute("KESKKYTE");
	static final Attribute GAASKYTE_ATTR = new Attribute("GAASKYTE");
	static final Attribute LATLON_ATTR = new Attribute("LATLON");
	static final Attribute KAMIN_ATTR = new Attribute("KAMIN");
	static final Attribute GARAZH_ATTR = new Attribute("GARAZH");
	static final Attribute SALES_DATE_ATTR = new Attribute("SALES_DATE");
	static final Attribute PRICE_PER_M2_ATTR = new Attribute("PRICE_PER_M2");
	
	static final Attribute SELECTED_PRICE_ATTR = PRICE_PER_M2_ATTR; //PRICE_ATTR;
	
	static FastVector getOsad() {
		FastVector my_nominal_values = new FastVector(10); 
	      my_nominal_values.addElement("Haabersti"); 
	      
	      my_nominal_values.addElement("Kadriorg"); 
	      my_nominal_values.addElement("Vanalinn"); 
	      my_nominal_values.addElement("Põhja-Tallinn"); 
	      my_nominal_values.addElement("Nõmme"); 
	      my_nominal_values.addElement("Kristiine"); 
	      my_nominal_values.addElement("Kesklinn"); 
	      my_nominal_values.addElement("Lasnamäe"); 
	      my_nominal_values.addElement("Pirita"); 
	      my_nominal_values.addElement("Mustamäe");
	      return my_nominal_values;
	      
	}
	static final Attribute LINNAOSA_ATTR = new Attribute("linnaosa", getOsad());//new Attribute("linnaosa");
	
	static class AccumulatorFunction implements Function<Stats> {
		public double averageDelta = 0.0d;
		public double maxDelta = Double.MIN_VALUE;
		public double minDelta = Double.MAX_VALUE;
		
		double sum = 0;
		long count = 0;
		
		public void exec(Stats stats) {
			count++;
			final double delta = stats.getAbsDelta();
			sum += delta;
			if (delta < minDelta) {
				minDelta = delta;
			}
			if (delta > maxDelta) {
				maxDelta = delta;
			}
		}
		
		public double getAverageDelta() {
			return sum / count;
		}

	};
	
	static class OsaStats {

		List<Double> deltas = new ArrayList<Double>();
		List<Double> percentageErrors = new ArrayList<Double>();
		
		public int getTotal() {
			return percentageErrors.size(); //TODO get from better source
		}
		
		public void addDelta(double delta) {
			deltas.add(delta);
			
		}

		public void addPercentageError(double percentageError) {
			percentageErrors.add(percentageError);
		}
		
		//For the osa, give the percentage of tests whose 
		//abs(guessedPrice - actualPrice) / actualPrice <= percentage
		public double getPercentageErrorWithin(double percentage) {
			int cnt = 0;
			for (double p : percentageErrors) {
				if (p <= percentage) {
					cnt++;
				}
			}
			return (Double.valueOf(cnt) / Double.valueOf(percentageErrors.size())) * 100.0d;
		}
		
		public double getAveragePercentageError() {
			double avg = 0;
			for (double v : percentageErrors) {
				avg += v / percentageErrors.size();
			}
			
			return avg;
		}
		
		public double getAverageDelta() {
			double avg = 0;
			for (double v : deltas) {
				avg += v / (double)(deltas.size());
			}
			
			return avg;
		}
		
	}
	
	/**
	 * Get stats per linnaosa
	 *
	 */
	static class OsaAccumulatorFunction implements Function<Stats> {
		public double averageDelta = 0.0d;
		public double maxDelta = Double.MIN_VALUE;
		public double minDelta = Double.MAX_VALUE;
		
		double sum = 0;
		long count = 0;
		
		Map<String, OsaStats> osaStats = new HashMap<String, OsaStats>();
		
		public void exec(Stats stats) {
			final String osa = stats.dataPoint.getLinnaOsa();
			if (!osaStats.containsKey(osa)) {
				osaStats.put(osa, new OsaStats());
			}
			
			osaStats.get(osa).addDelta(stats.getDelta());
			osaStats.get(osa).addPercentageError(stats.getPercentageError());
		}

	};
	
	public static Map<Attribute,Double> featureWeightMap(Map<Attribute, Double> featureImportance) {
		double featureImportanceSum = 0;
		//The feature weight w(p) is equal to 1 when feature importance is not used to weight distances; otherwise, it is equal to the normalized feature importance:
		//
		for (Map.Entry<Attribute,Double> entry : featureImportance.entrySet()) {
			featureImportanceSum += entry.getValue();
		}
		
		
		Map<Attribute,Double> weights = new HashMap<Attribute,Double>();
		for (Map.Entry<Attribute,Double> entry : featureImportance.entrySet()) {
			weights.put(entry.getKey(), entry.getValue() / featureImportanceSum);
		}
		return weights;
	}
	
	public static void main(String[] args) throws Exception {
		File mysqlFile = new File(args[0]);
		FlatStream flatStream = new FlatStream(mysqlFile);
		List<Flat> flats = new ArrayList<Flat>();
		final int totalSize = Integer.MAX_VALUE; //Limits total number of observations
		//final int totalSize = 10000;
		final int[] kRange = new int[]{6,6};
		
		final boolean NORMALIZE_DISTANCE = false;
		
		Map<Attribute, Double> featureImportance = new HashMap<Attribute, Double>();
		
		featureImportance.put(SAUN_ATTR, 2.0);
		featureImportance.put(RODU_ATTR, 1.0);
		featureImportance.put(LINNAOSA_ATTR, 4.0);	
		featureImportance.put(ELKKYTE_ATTR, 0.1);
		featureImportance.put(GAASKYTE_ATTR, 1.0);
		featureImportance.put(KESKKYTE_ATTR, 1.25);
		featureImportance.put(GARAZH_ATTR, 1.0);
		featureImportance.put(KAMIN_ATTR, 1.0);
		 	
		try {
			while(flatStream.hasNext() && flats.size() < totalSize  ) {
				Flat flat = flatStream.next();
				flats.add(flat);
			}
		} 
		catch (Exception x) {	
			}
			
		sortByDate(flats);
			
		final double percentageTrain = 0.90;
		final int numTrain = (int) (percentageTrain * flats.size()); 
		List<Flat> trainFlats = Collections.unmodifiableList(flats.subList(0, numTrain));
		List<Flat> testFlats = Collections.unmodifiableList(flats.subList(numTrain, flats.size()));
	 	
		
		for (int k = 1; k < kRange[1]; k++) {
			
			Map<Attribute,Double> weights = featureWeightMap(featureImportance);
			
			System.out.println("Total data point size: " + flats.size());
			System.out.println("Training size: " + trainFlats.size());
			System.out.println("Test size: " + testFlats.size());
			System.out.println("K (nearest neighbor count): " + k);
			System.out.println("Weights: " + weights);

			final Collection<Stats> stats = new ArrayList<Stats>();

			for (int i = 0; i < testFlats.size(); i++) {
				stats.add(computePriceStats(testFlats.get(i), trainFlats, k, weights));
			}

			AccumulatorFunction func = new AccumulatorFunction();
			forEach(stats, func);

			System.out.println("AverageDelta = " + func.getAverageDelta());
			System.out.println("MinDelta = " + func.minDelta);
			System.out.println("MaxDelta = " + func.maxDelta);

			System.out.println("Osa Stats");
			OsaAccumulatorFunction osaFunc = new OsaAccumulatorFunction();
			forEach(stats, osaFunc);
			for (Map.Entry<String, OsaStats> osaStats : osaFunc.osaStats
					.entrySet()) {
				System.out.println(osaStats.getKey());
				System.out.println("\tDataPoint count: "
						+ osaStats.getValue().getTotal());
				System.out.println("\t AverageDelta: "
						+ osaStats.getValue().getAverageDelta());
				System.out.println("\t AveragePercentageError: "
						+ osaStats.getValue().getAveragePercentageError());
				System.out.println("\t Percentage Within 5%: "
						+ osaStats.getValue().getPercentageErrorWithin(5));
				System.out.println("\t Percentage Within 15%: "
						+ osaStats.getValue().getPercentageErrorWithin(15));
				System.out.println("\t Percentage Within 25%: "
						+ osaStats.getValue().getPercentageErrorWithin(25));
			}
		}
	    
	}
	
	
	

	
	private static void sortByDate(List<Flat> flats) {
		Collections.sort(flats, new Comparator<Flat>() {

			@Override
			public int compare(Flat arg0, Flat arg1) {
				return arg0.getSalesDate().compareTo(arg1.getSalesDate());
			}
			
		});
		
	}

	static class Stats {
		
		double actualPrice;
		double estimatedPrice;
		Flat dataPoint;
		
		public Stats(Flat dataPoint, double estimatedPrice) {
			this.dataPoint = dataPoint;
			this.actualPrice = dataPoint.getPricePerM2();
			this.estimatedPrice = estimatedPrice;
		}
		
		double getAbsDelta() {
			return Math.abs(estimatedPrice - actualPrice); 
		}
		
		double getDelta() {
			return Math.abs(estimatedPrice - actualPrice); 
		}
		
		double getPercentageError() {
			return Math.abs(getAbsDelta() / actualPrice) * 100.0d;
		}
		
	}
	
	private static Stats computePriceStats(Flat flat, List<Flat> trainFlats,
			int k, Map<Attribute,Double> weights) throws Exception {
	        
        Collection<Flat> neighbors = kNeighborSearch(k, flat, trainFlats, weights);
        
        //Right now lets have price be average of the neighbors
        double priceEstimate = 0;

        for (Flat neighbor : neighbors) {
        	priceEstimate += priceNormalized(neighbor, flat.getSalesDate()) / neighbors.size();
        }
        
        return new Stats(flat, priceEstimate);
	}
	
	private static double priceNormalized(Flat neighbor, Date adjustDate) {
		return priceIndex.getAdjustedPrice(neighbor.getPricePerM2(), neighbor.getSalesDate(), adjustDate);
		//return (Double)metaDataMap.get(id(instance)).get(SELECTED_PRICE_ATTR);
	}
	
	private static int id(Instance inst) {
		return (int)inst.value(ID_ATTR);
	}
	
	static Map<Integer, Map<Attribute, Object>> metaDataMap  = new HashMap<Integer, Map<Attribute, Object>>();
	static int idCnt = 0;
	
	public static Instances createInstances(String dataName, Collection<Flat> flats, Set<Attribute> selectedAttributes) {
 	    
		final int attrCount = selectedAttributes.size() + 1;

		FastVector attributes = new FastVector(attrCount);
 	    
 	    attributes.addElement(ID_ATTR);
 	    
 	    for (Attribute attr : selectedAttributes) {
 	    	attributes.addElement(attr);
 	    }
 
 	    Instances wekaPoints = new Instances(dataName, attributes, 0);
 	    wekaPoints.setClass(ID_ATTR);
 	    
 	    for (Flat flat : flats) {
 	    	int id = idCnt++;
			Instance inst = new Instance(attrCount);
			inst.setValue(ID_ATTR, id);
			inst.setValue(SAUN_ATTR, flat.isSaun() ? 10 : 0);
	        inst.setValue(RODU_ATTR, flat.isRodu() ? 10 : 0);
	        //inst.setValue(LATLON_ATTR, flat.getLatlon());
	        inst.setValue(LINNAOSA_ATTR, flat.getLinnaOsa());
	     
	        
	        Map<Attribute, Object> metaData = new HashMap<Attribute, Object>();
	        metaData.put(YLDPIND_ATTR, flat.getYldpind());
	        metaData.put(PRICE_PER_M2_ATTR, flat.getPricePerM2());
	        metaData.put(LINNAOSA_ATTR, flat.getLinnaOsa());
	        metaData.put(SALES_DATE_ATTR, flat.getSalesDate());
	        metaDataMap.put(id(inst), metaData);
	        
	        inst.setDataset(wekaPoints);
	        wekaPoints.add(inst);
 	    }
 	    
 	    return wekaPoints;
	}
	

	public static class DistanceEntry implements Comparable<DistanceEntry> {
		
		private Flat flat;
		private double distance;

		public DistanceEntry(Flat flat, double distance) {
			this.flat = flat;
			this.distance = distance;
		}

		@Override
		public int compareTo(DistanceEntry arg0) {
			if (this.distance < arg0.distance) {
				return -1;
			} else if (this.distance > arg0.distance) {
				return 1;
			} else {
				return 0;
			}
		}
		
	}
	
	static Comparator<DistanceEntry> flatComparator = new Comparator<DistanceEntry>() {

		@Override
		public int compare(DistanceEntry o1, DistanceEntry o2) {
			return o1.compareTo(o2);
		}

		
	};
	
	public static Collection<Flat> kNeighborSearch(final int k, 
												   Flat first, 
												   Collection<Flat> rest, 
												   Map<Attribute, Double> weights) {
		
		List<DistanceEntry> distances = new ArrayList<DistanceEntry>(rest.size());
		
		for (Flat flat : rest) {
			if (first != flat) {
				distances.add(new DistanceEntry(flat, first.distance(flat, weights)));
			}
		}
		
		Collections.sort(distances, flatComparator);
		
		List<Flat> neighbors = new ArrayList<Flat>();
		
		//output at least k neighbors. 
		
		for (int i = 0; i < k; i++) {
			neighbors.add(distances.get(i).flat);
		}
		double lastDistance = distances.get(k-1).distance;
		
		//If there are more equal in distance to the kth, output those as well.
		int n = k;
		while (n < distances.size()) {
			DistanceEntry entry = distances.get(n++);
			if (entry.distance != lastDistance) {
				break;
			}
			neighbors.add(entry.flat);
		}
		
		return neighbors;
		
	}


	
	

}
